/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.sedona.common.spider;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.junit.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

public class GeneratorTest {
  private static final double DELTA = 1e-10;

  @Test(expected = IllegalArgumentException.class)
  public void testInvalidGenerator() {
    GeneratorFactory.create("invalid", new Random(), new HashMap<>());
  }

  @Test
  public void testPointGeneration() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    Random random = new Random(42);

    String[] generators = {"uniform", "gaussian", "bit", "diagonal", "sierpinski"};

    for (String generatorName : generators) {
      Generator generator = GeneratorFactory.create(generatorName, random, params);

      // Generate and verify points
      for (int i = 0; i < 10; i++) {
        Geometry geom = generator.next();
        assertTrue("Point generation failed for " + generatorName, geom instanceof Point);
        Point point = (Point) geom;
        assertTrue("Invalid point generated by " + generatorName, point.isValid());
        // All generators should produce points within [0,1] range
        assertTrue(
            "X coordinate out of bounds for " + generatorName,
            point.getX() >= -0.5 && point.getX() <= 1.5);
        assertTrue(
            "Y coordinate out of bounds for " + generatorName,
            point.getY() >= -0.5 && point.getY() <= 1.5);
      }
    }
  }

  @Test
  public void testPolygonGeneration() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "polygon");
    params.put("maxSize", "0.1");
    params.put("minSegments", "4");
    params.put("maxSegments", "6");
    Random random = new Random(42);

    String[] generators = {"uniform", "gaussian", "bit", "diagonal", "sierpinski"};

    for (String generatorName : generators) {
      Generator generator = GeneratorFactory.create(generatorName, random, params);

      // Generate and verify polygons
      for (int i = 0; i < 10; i++) {
        Geometry geom = generator.next();
        assertTrue("Polygon generation failed for " + generatorName, geom instanceof Polygon);
        Polygon polygon = (Polygon) geom;
        assertTrue(
            "Polygon has too few points for " + generatorName,
            polygon.getCoordinates().length >= 5); // At least 4 points + closing point
        assertTrue(
            "Polygon has too many points for " + generatorName,
            polygon.getCoordinates().length <= 7); // At most 6 points + closing point

        // Verify polygon bounds
        assertTrue(
            "Polygon out of bounds for " + generatorName,
            polygon.getEnvelopeInternal().getMinX() >= -0.5
                && polygon.getEnvelopeInternal().getMaxX() <= 1.5
                && polygon.getEnvelopeInternal().getMinY() >= -0.5
                && polygon.getEnvelopeInternal().getMaxY() <= 1.5);

        // Verify polygon area
        double area = polygon.getArea();
        assertTrue("Polygon area exceeds maximum for " + generatorName, area <= 0.01);
      }
    }
  }

  @Test
  public void testBoxGeneration() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "box");
    params.put("maxWidth", "0.1");
    params.put("maxHeight", "0.1");
    Random random = new Random(42);

    String[] generators = {"uniform", "gaussian", "bit", "diagonal", "sierpinski"};

    for (String generatorName : generators) {
      Generator generator = GeneratorFactory.create(generatorName, random, params);

      // Generate and verify boxes
      for (int i = 0; i < 10; i++) {
        Geometry geom = generator.next();
        assertTrue("Box generation failed for " + generatorName, geom instanceof Polygon);
        Polygon box = (Polygon) geom;
        assertTrue("Invalid box generated by " + generatorName, box.isValid());
        assertTrue("Non-rectangular box generated by " + generatorName, box.isRectangle());
        assertEquals(
            "Incorrect number of points in box for " + generatorName,
            5,
            box.getCoordinates().length); // 4 corners + closing point

        // Verify box bounds
        assertTrue(
            "Box out of bounds for " + generatorName,
            box.getEnvelopeInternal().getMinX() >= -0.5
                && box.getEnvelopeInternal().getMaxX() <= 1.5
                && box.getEnvelopeInternal().getMinY() >= -0.5
                && box.getEnvelopeInternal().getMaxY() <= 1.5);

        // Verify box dimensions
        double width = box.getEnvelopeInternal().getWidth();
        double height = box.getEnvelopeInternal().getHeight();
        assertTrue("Box width exceeds maximum for " + generatorName, width <= 0.1);
        assertTrue("Box height exceeds maximum for " + generatorName, height <= 0.1);
      }
    }
  }

  @Test
  public void testUniformGenerator() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    Random random = new Random(42); // Fixed seed for reproducibility

    Generator generator = GeneratorFactory.create("uniform", random, params);
    assertTrue(generator.hasNext());

    // Generate and verify points are within [0,1] range
    for (int i = 0; i < 100; i++) {
      Point point = (Point) generator.next();
      assertTrue(point.getX() >= 0 && point.getX() <= 1);
      assertTrue(point.getY() >= 0 && point.getY() <= 1);
    }
  }

  @Test
  public void testGaussianGenerator() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("gaussian", random, params);

    // Generate points and verify they cluster around 0.5,0.5
    double sumX = 0, sumY = 0;
    int count = 1000;

    for (int i = 0; i < count; i++) {
      Point point = (Point) generator.next();
      sumX += point.getX();
      sumY += point.getY();
    }

    // Verify mean is close to 0.5
    assertEquals(0.5, sumX / count, 0.1);
    assertEquals(0.5, sumY / count, 0.1);
  }

  @Test
  public void testSierpinskiGeneratorBasic() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("sierpinski", random, params);

    // First three points should be the triangle vertices
    Point p1 = (Point) generator.next();
    Point p2 = (Point) generator.next();
    Point p3 = (Point) generator.next();

    // Verify first point is at (0,0)
    assertEquals(0.0, p1.getX(), DELTA);
    assertEquals(0.0, p1.getY(), DELTA);

    // Verify second point is at (1,0)
    assertEquals(1.0, p2.getX(), DELTA);
    assertEquals(0.0, p2.getY(), DELTA);

    // Verify third point is at (0.5, sqrt(3)/2)
    assertEquals(0.5, p3.getX(), DELTA);
    assertEquals(Math.sqrt(3) / 2, p3.getY(), DELTA);
  }

  @Test
  public void testSierpinskiGenerator() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("sierpinski", random, params);

    // Generate 1000 points and verify they're within the triangle bounds
    for (int i = 0; i < 1000; i++) {
      Point point = (Point) generator.next();

      // All points should be within the bounds of the initial triangle
      assertTrue(point.getX() >= 0 && point.getX() <= 1);
      assertTrue(point.getY() >= 0 && point.getY() <= Math.sqrt(3) / 2);
    }
  }

  @Test
  public void testBitGenerator() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    params.put("probability", "0.5");
    params.put("digits", "5");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("bit", random, params);

    // Generate points and verify they're within [0,1] range
    for (int i = 0; i < 100; i++) {
      Point point = (Point) generator.next();
      assertTrue(point.getX() >= 0 && point.getX() <= 1);
      assertTrue(point.getY() >= 0 && point.getY() <= 1);
    }
  }

  @Test
  public void testDiagonalGenerator() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    params.put("percentage", "1.0"); // Force all points to be on diagonal
    params.put("buffer", "0.1");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("diagonal", random, params);

    // With percentage=1.0, all points should be exactly on the diagonal
    for (int i = 0; i < 100; i++) {
      Point point = (Point) generator.next();
      assertEquals(point.getX(), point.getY(), DELTA);
      assertTrue(point.getX() >= 0 && point.getX() <= 1);
    }
  }

  @Test
  public void testDiagonalGeneratorWithBuffer() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "point");
    params.put("percentage", "0.5"); // Only half of the points will be on the diagonal
    params.put("buffer", "0.1");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("diagonal", random, params);

    // With percentage=0.5, half of the points should be exactly on the diagonal
    int onDiagonalCount = 0;
    for (int i = 0; i < 100; i++) {
      Point point = (Point) generator.next();
      if (Math.abs(point.getX() - point.getY()) < DELTA) {
        onDiagonalCount++;
      }
      assertTrue(point.getX() >= 0 && point.getX() <= 1);
    }
    // Expect around half of the points to be on the diagonal
    assertEquals(50, onDiagonalCount, 10);
  }

  @Test
  public void testParcelGeneratorBasic() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "box");
    params.put("dither", "0.0");
    params.put("splitRange", "0.5");
    Random random = new Random(42);

    int cardinalities[] = {0, 1, 3, 10, 100};

    for (int cardinality : cardinalities) {
      params.put("cardinality", Integer.toString(cardinality));
      Generator generator = GeneratorFactory.create("parcel", random, params);

      // Generate and verify all boxes
      List<Geometry> boxes = new ArrayList<>();
      while (generator.hasNext()) {
        boxes.add(generator.next());
      }

      // Verify cardinality
      assertEquals(cardinality, boxes.size());

      // Verify all boxes are valid and within bounds
      for (Geometry box : boxes) {
        assertTrue(box.isValid());
        assertTrue(box.isRectangle());
        assertTrue(box.getEnvelopeInternal().getMinX() >= 0);
        assertTrue(box.getEnvelopeInternal().getMaxX() <= 1);
        assertTrue(box.getEnvelopeInternal().getMinY() >= 0);
        assertTrue(box.getEnvelopeInternal().getMaxY() <= 1);
      }

      // Verify that the boxes are not overlapping with each other
      for (int i = 0; i < boxes.size(); i++) {
        for (int j = i + 1; j < boxes.size(); j++) {
          assertFalse(boxes.get(i).overlaps(boxes.get(j)));
        }
      }
    }
  }

  @Test
  public void testParcelGeneratorDither() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "box");
    params.put("cardinality", "10");
    params.put("dither", "0.5"); // High dither value
    params.put("splitRange", "0.5");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("parcel", random, params);

    // Generate boxes and calculate total area
    double totalArea = 0;
    while (generator.hasNext()) {
      Geometry box = generator.next();
      totalArea += box.getArea();
    }

    // With high dither, total area should be less than 1.0 (unit square)
    assertTrue("Total area with dither should be less than 1.0", totalArea < 1.0);
  }

  @Test
  public void testParcelGeneratorSplitRange() {
    // Test with minimum split range (more varied box sizes)
    Map<String, String> params1 = new HashMap<>();
    params1.put("geometryType", "box");
    params1.put("cardinality", "20");
    params1.put("dither", "0.0");
    params1.put("splitRange", "0.0");
    Random random1 = new Random(42);

    Generator generator1 = GeneratorFactory.create("parcel", random1, params1);
    List<Double> areas1 = new ArrayList<>();
    while (generator1.hasNext()) {
      areas1.add(generator1.next().getArea());
    }

    // Test with maximum split range (more uniform box sizes)
    Map<String, String> params2 = new HashMap<>();
    params2.put("geometryType", "box");
    params2.put("cardinality", "20");
    params2.put("dither", "0.0");
    params2.put("splitRange", "0.5");
    Random random2 = new Random(42);

    Generator generator2 = GeneratorFactory.create("parcel", random2, params2);
    List<Double> areas2 = new ArrayList<>();
    while (generator2.hasNext()) {
      areas2.add(generator2.next().getArea());
    }

    // Calculate variance of areas
    double variance1 = calculateVariance(areas1);
    double variance2 = calculateVariance(areas2);

    // Variance should be higher with splitRange=0.0
    assertTrue("Variance with splitRange=0.0 should be higher", variance1 > variance2);
  }

  @Test
  public void testParcelGeneratorLargeCardinality() {
    Map<String, String> params = new HashMap<>();
    params.put("geometryType", "box");
    params.put("cardinality", "1000");
    params.put("dither", "0.0");
    params.put("splitRange", "0.5");
    Random random = new Random(42);

    Generator generator = GeneratorFactory.create("parcel", random, params);

    int count = 0;
    double totalArea = 0;
    while (generator.hasNext()) {
      Geometry box = generator.next();
      count++;
      totalArea += box.getArea();
    }

    assertEquals(1000, count);
    assertEquals(1.0, totalArea, 1e-10); // Total area should be 1.0 with no dither
  }

  // Helper method to calculate variance
  private double calculateVariance(List<Double> values) {
    double mean = values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
    return values.stream().mapToDouble(v -> Math.pow(v - mean, 2)).average().orElse(0.0);
  }
}
